// MplayerWidget.cs 
// User: mhull at 11:54 01/23/2008
//(c) 2007 Mike Hull

using System;
using System.IO;
using System.Threading;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Gdk;
using Gtk;

namespace Mplayer
{
	/// <summary>
	/// Mplayer Widget runs the mplayer application on a Gdk Widget
	/// </summary>
	public partial class MplayerWidget : Gtk.EventBox
	{
		string mplayerPath = "/usr/bin/mplayer";
		
		string optionsFormat = "-wid {0} -vo {1} -msglevel all=4 {2} {3}";
		string otherOptions = "";

		Thread outputThread;
		Thread errorThread;
		
		string filename = null;
		
		MplayerVideoOutputs videoOutputFormat = MplayerVideoOutputs.Xv;
		
		Process process;

		int xid = -1;
		int bufferSize = 8192;
		
		int videoHeight, videoWidth;
		
		Gtk.Socket videoSocket;
		
//		[DllImport ("gdk-x11-2.0")]
//		static extern IntPtr gdk_x11_drawable_get_xid(IntPtr window);
		
		/// <value>
		/// Xid after the widget has been realized.  Value is -1 prior to initialization.
		/// </value>
		public int Xid {
			get {
				return xid;
			}
		}

		/// <value>
		/// Path to mplayer executable.  Defaults to '/usr/bin/mplayer'
		/// </value>
		public string MplayerPath {
			get {
				return mplayerPath;
			}
			set {
				mplayerPath = value;
			}
		}

		/// <value>
		/// Options that mplayer is launched with.
		/// </value>
		protected virtual string Options {
			get { 
				return string.Format(optionsFormat, 
				                     Xid,
				                     MplayerWidget.FromVideoOutputFormat(VideoOutputFormat),
				                     otherOptions ?? string.Empty,
				                     string.IsNullOrEmpty(filename) ? "-": filename); 
			}
		}

		/// <value>
		/// Video Output Format (X11, Gl, Xv)
		/// </value>
		public MplayerVideoOutputs VideoOutputFormat {
			get {
				return videoOutputFormat;
			}
			set {
				videoOutputFormat = value;
			}
		}
		
		/// <value>
		/// Other command line options to pass to mplayer
		/// </value>
		public string OtherOptions {
			get {
				return otherOptions;
			}
			set {
				otherOptions = value;
			}
		}

		/// <value>
		/// How many bytes to read from a video stream at a time.  Also used to calculate the buffered stream buffer size as BufferSize * 10.
		/// <remarks>This is not sent to the command line of mplayer, if you want to send adjust mplayers default buffers, use the OtherOptions property.</remarks> 
		/// </value>
		public int BufferSize {
			get {
				return bufferSize;
			}
			set {
				bufferSize = value;
			}
		}

		/// <value>
		/// True if you can control mplayer, only false when you are playing a Stream or writing to standard input of mplayer
		/// </value>
		public bool CanControl {
			get { 
				return (this.filename != null && 
				        this.process != null && 
				        this.process.StandardInput.BaseStream.CanWrite); 
			}
		}
		
		/// <summary>
		/// Event is raised the video is stopped.
		/// </summary>
		public event EventHandler Stopped;
		
		/// <summary>
		/// Default MplayerWidget constructor.
		/// </summary>
		public MplayerWidget()
		{
			this.Build();
			this.videoSocket = new Socket();
			this.videoSocket.Realized += new EventHandler(OnVideoWidgetRealized);
			this.fixed1.Put(this.videoSocket, 0, 0);
		}
		
		protected virtual void OnVideoWidgetRealized (object sender, EventArgs args)
		{				
			this.xid = (int)videoSocket.Id;
#if TRACE 
			System.Diagnostics.Trace.TraceInformation("XID: {0}", this.xid);
#endif
		}
		
		protected override void OnSizeAllocated (Rectangle allocation)
		{
			base.OnSizeAllocated (allocation);

			if (this.videoHeight > 0 && this.videoWidth > 0)
			{
				Rectangle rect = MplayerWidget.CalculateSize(allocation, videoWidth, videoHeight);
				if (this.videoSocket.HeightRequest != rect.Height && 
				    this.videoSocket.WidthRequest != rect.Width)
				{
					this.fixed1.Move(videoSocket, rect.X, rect.Y);
					this.videoSocket.SetSizeRequest(rect.Width, rect.Height);
				}
				System.Diagnostics.Trace.TraceInformation("X:'{0}' Y:'{1}' W:'{2}' H:'{3}'", 
				                                          rect.X, rect.Y, rect.Width, rect.Height);
			}
		}
		
		
		
		/// <summary>
		/// Play a file or url.
		/// </summary>
		/// <param name="filename">
		/// A <see cref="System.String"/> that represents the file or the url for the mplayer widget to play.
		/// </param>
		public void Play(string filename)
		{
			if (string.IsNullOrEmpty(filename))
				throw new ArgumentException("Filename must not be null or empty.");
			
			this.filename = filename;
			
			startMplayer();
		}
		
		/// <summary>
		/// Play a streamable video.
		/// </summary>
		/// <param name="stream">
		/// A <see cref="Stream"/> that represents a streamable video format.
		/// </param>
		public void Play(Stream stream)
		{
			stream = new BufferedStream(stream, bufferSize * 10);
			byte[] buffer = new byte[bufferSize];
			try {
				stream.BeginRead(buffer, 0, bufferSize, readCallback, new object[] { stream, buffer, Play() });
			} catch (Exception ex) {
#if TRACE
				System.Diagnostics.Trace.TraceError("Error beginning read from stream: {0}", ex);
#endif
				Stop();
				throw;
			}
		}
		
		private void readCallback(IAsyncResult result)
		{
			object[] state = (object[])result.AsyncState;
			
			Stream stream = (Stream)state[0];
			
			int count;
			try {
				count = stream.EndRead(result);
			} catch (Exception ex) {
#if TRACE
				System.Diagnostics.Trace.TraceError("Error reading from stream: {0}", ex);
#endif
				count = -1;
			}
			
			Stream sw = (Stream)state[2];
			
			if (count > 0 && process != null && sw.CanWrite) {
				byte[] buffer = (byte[])state[1];
				
				try {
					sw.Write(buffer, 0, count);
				} catch (Exception ex) {
#if TRACE
					System.Diagnostics.Trace.TraceError("Error writing to mplayer: {0}", ex);
#endif					
					Stop();
				}
				
				try {
					stream.BeginRead(buffer, 0, buffer.Length, readCallback, state);
				} catch (Exception ex) {
#if TRACE
					System.Diagnostics.Trace.TraceError("Error beginning read from stream: {0}", ex);
#endif
					Stop();
				}
				
			} else {
				Stop();
			}
		}
		
		/// <summary>
		/// Starts an mplayer process for streaming a video to.
		/// </summary>
		/// <returns>
		/// A <see cref="Stream"/> that writes to the standard input stream of the mplayer process.
		/// </returns>
		public Stream Play()
		{
			this.filename = null;
			startMplayer();
			return process.StandardInput.BaseStream;
		}
		
		private void startMplayer()
		{
			if (xid == -1)
				throw new InvalidOperationException("Widget must be shown prior to playing video.");

			if (process != null)
				Stop();
			
			ProcessStartInfo psi = new ProcessStartInfo(MplayerPath, this.Options);
			psi.RedirectStandardInput = true; 
			psi.RedirectStandardOutput = true;
			psi.RedirectStandardError = true;
			psi.UseShellExecute = false;
			
			process = new Process();
			process.StartInfo = psi;
			process.Exited += new EventHandler(processExited);
			process.Start();

			outputThread = new Thread(readOutputProc);
			errorThread = new Thread(readErrorProc);
			outputThread.Start();
			errorThread.Start();
		}
		
		private void readErrorProc()
		{
			while (process != null && !process.HasExited)
			{
				string line = null;
				try {
					line = process.StandardError.ReadLine();
				} catch (Exception) {
					break;
				}
				
#if TRACE && MPLAYER_TRACE
				System.Diagnostics.Trace.TraceError(line);
#endif
				
				if (line == null)
					break;
				else if (line.StartsWith("X11 error: BadDrawable"))
				{
					Stop();			
					break;
				}
			}
		}
		
		private void readOutputProc()
		{
			while (process != null && !process.HasExited)
			{
				string line = null;
				try {
					line = process.StandardOutput.ReadLine();
				} catch (Exception) {
					break;
				}
				
#if TRACE && MPLAYER_TRACE
				System.Diagnostics.Trace.WriteLine(line);
#endif
				
				if (line == null)
					break;
				else if (line.StartsWith("VIDEO:") && line.Contains("]") && line.Contains("x"))
				{
					//VIDEO:  [MP4S]  320x240  24bpp  1000.000 fps    0.0 kbps ( 0.0 kbyte/s)
					string data = line.Substring(line.IndexOf("]") + 1).Trim();
					int x = data.IndexOf("x");
					if (x > 0) {
						string width = data.Substring(0, data.IndexOf("x")).Trim();
						int w, h;
						if (int.TryParse(width, out w))
						{
							string height = data.Substring(++x, data.IndexOf(" ", x) - x).Trim();
							if (int.TryParse(height, out h))
							{
#if TRACE
								System.Diagnostics.Trace.TraceInformation("Video Size: {0}x{1}", w, h);
#endif
							
								this.videoHeight = h;
								this.videoWidth = w;
								Application.Invoke(new EventHandler(delegate {								
									this.SetSizeRequest(w, h);
								}));
							}
						}
					}
				}
				//TODO: track progress and other stats by increasing to msglevel 5
				//V:  17.4 17395/17395  2%  0%  0.0% 0 0
				//V: 120.5 120512/120512  1%  0%  0.0% 0 0 0.91x 

			}
		}
				
		private void processExited(object sender, EventArgs args)
		{
			Stop();
			process = null;
		}
		
		protected virtual void OnStopped(object sender, EventArgs args)
		{
			if (Stopped != null)
				Stopped(sender, args);
		}

		/// <summary>
		/// Stop Mplayer Process
		/// </summary>
		public void Stop()
		{
			try {
				if (process != null && !process.HasExited) { 
					Mono.Unix.Native.Syscall.kill(process.Id, Mono.Unix.Native.Signum.SIGABRT);
					process = null;
					if (outputThread != null)
					{
						this.outputThread.Abort();
						this.errorThread.Abort();
						this.outputThread = null;
						this.errorThread = null;
					}
					OnStopped(this, new EventArgs());
				}
			} catch (Exception ex) {
				System.Diagnostics.Trace.TraceError("Error stopping mplayer: {0}", ex);
			}
		}

		public void WriteToStandardInput(params char[] chars)
		{
			if (CanControl)
			{
				process.StandardInput.Write(chars);
			}
		}
		
		public void Mute()
		{
			WriteToStandardInput('m');
		}
		
		public void IncreaseVolume()
		{
			WriteToStandardInput('0');
		}
		
		public void DecreaseVolume()
		{
			WriteToStandardInput('9');
		}
		
		public void SkipForward()
		{
			WriteToStandardInput((char)0x1b, (char)0x5b, (char)0x43);
		}
		
		public void SkipBackward()
		{
			WriteToStandardInput((char)0x1b, (char)0x5b, (char)0x44);
		}
		
		public void Pause()
		{
			WriteToStandardInput(' ');
		}
		
		public void IncreaseSpeed()
		{
			WriteToStandardInput(']');
		}
		
		public void DecreaseSpeed()
		{
			WriteToStandardInput('[');
		}
		
		protected override bool OnDeleteEvent (Event evnt)
		{
			Stop();
			return base.OnDeleteEvent (evnt);
		}
 
		
		protected override void OnDestroyed ()
		{
			Stop();
			base.OnDestroyed ();
		}

		
		protected override bool OnDestroyEvent (Event evnt)
		{
			Stop();
			return base.OnDestroyEvent (evnt);
		}
		
		protected static string FromVideoOutputFormat(MplayerVideoOutputs outputFormat)
		{
			switch (outputFormat) {
			case MplayerVideoOutputs.Gl:
				return "gl";
			case MplayerVideoOutputs.Xv:
				return "xv";
			}
			return "x11";
		}
		
		protected static Rectangle CalculateSize(Rectangle maxSize, int width, int height)
		{
			double aspectRatio = (double)width / (double)height;
			double windowRatio = (double)maxSize.Width / (double)maxSize.Height;

			if (aspectRatio == windowRatio) {
				//alreadyCorrect!
				maxSize.Y = maxSize.X = 0;
			} else if (aspectRatio > windowRatio) {
				height = (int)((double)maxSize.Width / aspectRatio);
				maxSize.Y = (maxSize.Height - height) / 2;
				maxSize.X = 0;
				maxSize.Height = height; 

			} else {
				width = (int)((double)maxSize.Height * aspectRatio);
				maxSize.X = (maxSize.Width - width) / 2;
				maxSize.Y = 0;
				maxSize.Width = width;
			}

//			System.Diagnostics.Trace.TraceInformation("CalculateSize - X:{0},Y:{1},W:{2},H:{3},R:{4},MR:{5}", 
//			                                          maxSize.X, maxSize.Y, maxSize.Width, maxSize.Height, r, mr);		
			return maxSize;
		}	
	}
}
